<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <p data-lake-id="u27849b9b" id="u27849b9b"><span data-lake-id="u2a2625a0" id="u2a2625a0">相信很多人对Java中的注解都很熟悉，比如我们经常会用到的一些如@Override、@Autowired、@Service等，这些都是JDK或者诸如Spring这类框架给我们提供的。</span></p>
  <p data-lake-id="u90771e25" id="u90771e25"><span data-lake-id="udd185bca" id="udd185bca">​</span><br></p>
  <p data-lake-id="u1b2593c1" id="u1b2593c1"><span data-lake-id="udb11da4b" id="udb11da4b">在以往的面试过程中，我发现，关于注解的知识很多程序员都仅仅停留在使用的层面上，很少有人知道注解是如何实现的，更别提使用自定义注解来解决实际问题了。</span></p>
  <p data-lake-id="ud96c653f" id="ud96c653f"><span data-lake-id="u962eac60" id="u962eac60">​</span><br></p>
  <p data-lake-id="ufabaea1e" id="ufabaea1e"><span data-lake-id="ua84537d2" id="ua84537d2">但是其实，我觉得一个好的程序员的标准就是懂得如何优化自己的代码，那在代码优化上面，如何精简代码，去掉重复代码就是一个至关重要的话题，在这个话题领域，自定义注解绝对可以算得上是一个大大的功臣。</span></p>
  <p data-lake-id="u8ac55830" id="u8ac55830"><span data-lake-id="udacdbb0e" id="udacdbb0e">​</span><br></p>
  <p data-lake-id="u68833a96" id="u68833a96"><span data-lake-id="u6f01f7d8" id="u6f01f7d8">介绍几个，作者在开发中实际用到的几个例子，向你介绍下如何使用注解来提升你代码的逼格。</span></p>
  <p data-lake-id="u3ad10d08" id="u3ad10d08"><span data-lake-id="ue949bbcf" id="ue949bbcf">​</span><br></p>
  <h3 data-lake-id="R0DBu" id="R0DBu"><span data-lake-id="u7626720c" id="u7626720c">一、使用自定义注解做日志记录</span></h3>
  <p data-lake-id="uf69a6bd1" id="uf69a6bd1"><span data-lake-id="u4fdb3226" id="u4fdb3226">​</span><br></p>
  <p data-lake-id="uba8747c3" id="uba8747c3"><span data-lake-id="uc7e0d776" id="uc7e0d776">不知道大家有没有遇到过类似的诉求，就是希望在一个方法的入口处或者出口处做统一的日志处理，比如记录一下入参、出参、记录下方法执行的时间等。</span></p>
  <p data-lake-id="ub9be48e0" id="ub9be48e0"><span data-lake-id="u8d09a1e5" id="u8d09a1e5">​</span><br></p>
  <p data-lake-id="u0652fdda" id="u0652fdda"><span data-lake-id="u514cf45f" id="u514cf45f">如果在每一个方法中自己写这样的代码的话，一方面会有很多代码重复，另外也容易被遗漏。</span></p>
  <p data-lake-id="u9f9c933b" id="u9f9c933b"><span data-lake-id="u5e1949ab" id="u5e1949ab">​</span><br></p>
  <p data-lake-id="u6fdc911a" id="u6fdc911a"><span data-lake-id="u076f8706" id="u076f8706">这种场景，就可以使用自定义注解+切面实现这个功能。</span></p>
  <p data-lake-id="ub1a0d441" id="ub1a0d441"><span data-lake-id="uf0260b8c" id="uf0260b8c">​</span><br></p>
  <p data-lake-id="u30bb274d" id="u30bb274d"><span data-lake-id="u5593f8e0" id="u5593f8e0">假设我们想要在一些web请求的方法上，记录下本次操作具体做了什么事情，比如新增了一条记录或者删除了一条记录等。</span></p>
  <p data-lake-id="u4f4c781a" id="u4f4c781a"><span data-lake-id="u6cf276a2" id="u6cf276a2">​</span><br></p>
  <p data-lake-id="u27addbf4" id="u27addbf4"><span data-lake-id="u83655d72" id="u83655d72">首先我们自定义一个注解：</span></p>
  <p data-lake-id="ufd7f7134" id="ufd7f7134"><span data-lake-id="u4e0ed1ce" id="u4e0ed1ce">​</span><br></p>
  <pre lang="java"><code>
/**
 * Operate Log 的自定义注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpLog {

    /**
     * 业务类型，如新增、删除、修改
     *
     * @return
     */
    public OpType opType();

    /**
     * 业务对象名称，如订单、库存、价格
     *
     * @return
     */
    public String opItem();

    /**
     * 业务对象编号表达式，描述了如何获取订单号的表达式
     *
     * @return
     */
    public String opItemIdExpression();
}
</code></pre>
  <p data-lake-id="u475d2740" id="u475d2740"><br></p>
  <p data-lake-id="ub52fd774" id="ub52fd774"><span data-lake-id="ucb6ac33c" id="ucb6ac33c">因为我们不仅要在日志中记录本次操作了什么，还需要知道被操作的对象的具体的唯一性标识，如订单号信息。</span></p>
  <p data-lake-id="uc35c834d" id="uc35c834d"><span data-lake-id="uaf5b864e" id="uaf5b864e">​</span><br></p>
  <p data-lake-id="ubac1a844" id="ubac1a844"><span data-lake-id="ua004b020" id="ua004b020">但是每一个接口方法的参数类型肯定是不一样的，很难有一个统一的标准，那么我们就可以借助Spel表达式，即在表达式中指明如何获取对应的对象的唯一性标识。</span></p>
  <p data-lake-id="u29ad9b16" id="u29ad9b16"><span data-lake-id="u0717f28a" id="u0717f28a">​</span><br></p>
  <p data-lake-id="ufa463fa5" id="ufa463fa5"><span data-lake-id="ucb0070ae" id="ucb0070ae">有了上面的注解，接下来就可以写切面了。主要代码如下：</span></p>
  <p data-lake-id="u21b8476d" id="u21b8476d"><span data-lake-id="u5e089b65" id="u5e089b65">​</span><br></p>
  <pre lang="java"><code>
/**
 * OpLog的切面处理类，用于通过注解获取日志信息，进行日志记录
 *
 * @author Hollis
 */
@Aspect
@Component
public class OpLogAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(OpLogAspect.class);

    @Autowired
    HttpServletRequest request;

    @Around("@annotation(com.hollis.annotation.OpLog)")
    public Object log(ProceedingJoinPoint pjp) throws Exception {

        Method method = ((MethodSignature)pjp.getSignature()).getMethod();
        OpLog opLog = method.getAnnotation(OpLog.class);

        Object response = null;

        try {
            // 目标方法执行
            response = pjp.proceed();
        } catch (Throwable throwable) {
            throw new Exception(throwable);
        } 

        if (StringUtils.isNotEmpty(opLog.opItemIdExpression())) {
            SpelExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(opLog.opItemIdExpression());

            EvaluationContext context = new StandardEvaluationContext();
            // 获取参数值
            Object[] args = pjp.getArgs();

            // 获取运行时参数的名称
            LocalVariableTableParameterNameDiscoverer discoverer
                = new LocalVariableTableParameterNameDiscoverer();
            String[] parameterNames = discoverer.getParameterNames(method);

            // 将参数绑定到context中
            if (parameterNames != null) {
                for (int i = 0; i &lt; parameterNames.length; i++) {
                    context.setVariable(parameterNames[i], args[i]);
                }
            }

            // 将方法的resp当做变量放到context中，变量名称为该类名转化为小写字母开头的驼峰形式
            if (response != null) {
                context.setVariable(
                    CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, response.getClass().getSimpleName()),
                    response);
            }

            // 解析表达式，获取结果
            String itemId = String.valueOf(expression.getValue(context));

            // 执行日志记录
            handle(opLog.opType(), opLog.opItem(), itemId);
        }

        return response;
    }


    private void handle(OpType opType,  String opItem, String opItemId) {
      // 通过日志打印输出
      LOGGER.info("opType = " + opType.name() +",opItem = " +opItem + ",opItemId = " +opItemId);
    }
}
</code></pre>
  <p data-lake-id="ufca00095" id="ufca00095"><br></p>
  <p data-lake-id="u4ac2e52a" id="u4ac2e52a"><span data-lake-id="u05cf5360" id="u05cf5360">以上切面中，有几个点需要大家注意的：</span></p>
  <p data-lake-id="ueade97f4" id="ueade97f4"><span data-lake-id="u3ddd1f34" id="u3ddd1f34">​</span><br></p>
  <p data-lake-id="u5bd069ef" id="u5bd069ef"><span data-lake-id="u07028cfb" id="u07028cfb">1、使用@Around注解来指定对标注了OpLog的方法设置切面。 </span></p>
  <p data-lake-id="u776d83a5" id="u776d83a5"><span data-lake-id="u7f300978" id="u7f300978">2、使用Spel的相关方法，通过指定的表示，从对应的参数中获取到目标对象的唯一性标识。 </span></p>
  <p data-lake-id="ud344cc99" id="ud344cc99"><span data-lake-id="u7f7d7c6f" id="u7f7d7c6f">3、再方法执行成功后，输出日志。</span></p>
  <p data-lake-id="u62d6e1bc" id="u62d6e1bc"><span data-lake-id="ud1bc651d" id="ud1bc651d">​</span><br></p>
  <p data-lake-id="uc68b13c5" id="uc68b13c5"><span data-lake-id="u8fc64245" id="u8fc64245">有了以上的切面及注解后，我们只需要在对应的方法上增加注解标注即可，如：</span></p>
  <p data-lake-id="u8e8fa812" id="u8e8fa812"><span data-lake-id="u4ec92270" id="u4ec92270">​</span><br></p>
  <pre lang="java"><code>
 @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#id")
public @ResponseBody
HashMap view(@RequestParam(name = "id") String id)
    throws Exception {
}
</code></pre>
  <p data-lake-id="u62d3669a" id="u62d3669a"><br></p>
  <p data-lake-id="ua7918ab5" id="ua7918ab5"><span data-lake-id="u7eef0b2e" id="u7eef0b2e">面这种是入参的参数列表中已经有了被操作的对象的唯一性标识，直接使用</span><span data-lake-id="ub37e4119" id="ub37e4119">#id</span><span data-lake-id="u66712458" id="u66712458">指定即可。</span></p>
  <p data-lake-id="uf35f8f5f" id="uf35f8f5f"><span data-lake-id="ub27a1799" id="ub27a1799">如果被操作的对象的唯一性标识不在入参列表中，那么可能是入参的对象中的某一个属性，用法如下：</span></p>
  <p data-lake-id="u90dc3637" id="u90dc3637"><span data-lake-id="uc1441191" id="uc1441191">​</span><br></p>
  <pre lang="java"><code>
 @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#orderVo.id")
public @ResponseBody
HashMap update(OrderVO orderVo)
    throws Exception {
}
</code></pre>
  <p data-lake-id="u0d1aeabe" id="u0d1aeabe"><br></p>
  <p data-lake-id="ubc07e474" id="ubc07e474"><span data-lake-id="u7a9fc20d" id="u7a9fc20d">以上，即可从入参的OrderVO对象的id属性的值获取。</span></p>
  <p data-lake-id="u2dcabc1f" id="u2dcabc1f"><span data-lake-id="u5af79766" id="u5af79766">​</span><br></p>
  <p data-lake-id="ud8972594" id="ud8972594"><span data-lake-id="u94c03349" id="u94c03349">如果我们要记录的唯一性标识，在入参中没有的话，应该怎么办呢？最典型的就是插入方法，插入成功之前，根本不知道主键ID是什么，这种怎么办呢？</span></p>
  <p data-lake-id="u8560913b" id="u8560913b"><span data-lake-id="uae443c02" id="uae443c02">​</span><br></p>
  <p data-lake-id="ua6e44706" id="ua6e44706"><span data-lake-id="ue31ab01e" id="ue31ab01e">我们上面的切面中，做了一件事情，就是我们把方法的返回值也会使用表达式进行一次解析，如果可以解析得到具体的值，可以是可以。如以下写法：</span></p>
  <p data-lake-id="u206869f4" id="u206869f4"><span data-lake-id="u2cd69d0d" id="u2cd69d0d">​</span><br></p>
  <pre lang="java"><code>
 @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
@OpLog(opType = OpType.QUERY, opItem = "order", opItemIdExpression = "#insertResult.id")
public @ResponseBody
InsertResult insert(OrderVO orderVo)
    throws Exception {

    return orderDao.insert(orderVo);
}
</code></pre>
  <p data-lake-id="uef8f4d39" id="uef8f4d39"><br></p>
  <p data-lake-id="udce26dfc" id="udce26dfc"><span data-lake-id="ue770619d" id="ue770619d">以上，就是一个简单的使用自定义注解+切面进行日志记录的场景。下面我们再来看一个如何使用注解做方法参数的校验。</span></p>
  <p data-lake-id="u5633d5c5" id="u5633d5c5"><span data-lake-id="u20930d7c" id="u20930d7c">​</span><br></p>
  <p data-lake-id="u122f4727" id="u122f4727"><span data-lake-id="ud1c6c063" id="ud1c6c063">​</span><br></p>
  <h3 data-lake-id="v2kjg" id="v2kjg" style="text-align: justify"><span data-lake-id="u48b004c4" id="u48b004c4">二、</span><span data-lake-id="u45a80fa6" id="u45a80fa6">使用自定义注解做前置检查</span></h3>
  <p data-lake-id="ue2d18792" id="ue2d18792"><br></p>
  <p data-lake-id="u2bfe2a7b" id="u2bfe2a7b"><span data-lake-id="ue1fdcbf2" id="ue1fdcbf2">当我们对外部提供接口的时候，会对其中的部分参数有一定的要求，比如某些参数值不能为空等。大多数情况下我们都需要自己主动进行校验，判断对方传入的值是否合理。</span></p>
  <p data-lake-id="u6c6845fb" id="u6c6845fb"><br></p>
  <p data-lake-id="u038f4eb4" id="u038f4eb4"><span data-lake-id="u005ad5a7" id="u005ad5a7">这里推荐一个使用HibernateValidator + 自定义注解 + AOP实现参数校验的方式。</span></p>
  <p data-lake-id="uc33850ee" id="uc33850ee"><br></p>
  <p data-lake-id="u9b8a7e87" id="u9b8a7e87"><span data-lake-id="uf6b36029" id="uf6b36029">首先我们会有一个具体的入参类，定义如下：</span></p>
  <p data-lake-id="u1b81a4b7" id="u1b81a4b7"><br></p>
  <pre lang="java"><code>
public class User {
    private String idempotentNo;
    @NotNull(
        message = "userName can't be null"
    )
    private String userName;
}
</code></pre>
  <p data-lake-id="u9bd78328" id="u9bd78328"><br></p>
  <p data-lake-id="uefc90f5d" id="uefc90f5d"><span data-lake-id="uf92d33f0" id="uf92d33f0">以上，对userName参数注明不能为null。</span></p>
  <p data-lake-id="udc7b03f0" id="udc7b03f0"><br></p>
  <p data-lake-id="u178bd6ca" id="u178bd6ca"><span data-lake-id="u5fa3f4b9" id="u5fa3f4b9">然后再使用hibernate validator定义一个工具类，用于做参数校验。</span></p>
  <p data-lake-id="ub7e51b0a" id="ub7e51b0a"><br></p>
  <pre lang="java"><code>
/**
 * 参数校验工具
 *
 * @author Hollis
 */
public class BeanValidator {

    private static Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(true)
        .buildValidatorFactory().getValidator();

    /**
     * @param object object
     * @param groups groups
     */
    public static void validateObject(Object object, Class&lt;?&gt;... groups) throws ValidationException {
        Set&lt;ConstraintViolation&lt;Object&gt;&gt; constraintViolations = validator.validate(object, groups);
        if (constraintViolations.stream().findFirst().isPresent()) {
            throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());
        }
    }
}
</code></pre>
  <p data-lake-id="u64f8c1f9" id="u64f8c1f9"><br></p>
  <p data-lake-id="u83b315c5" id="u83b315c5"><span data-lake-id="u6178aa7a" id="u6178aa7a">以上代码，会对一个bean进行校验，一旦失败，就会抛出ValidationException。</span></p>
  <p data-lake-id="u97843b31" id="u97843b31"><br></p>
  <p data-lake-id="u0bc58520" id="u0bc58520"><span data-lake-id="u72536a47" id="u72536a47">接下来定义一个注解：</span></p>
  <p data-lake-id="ue39f6934" id="ue39f6934"><br></p>
  <pre lang="java"><code>
/**
 * facade接口注解， 用于统一对facade进行参数校验及异常捕获
 *
</code></pre>
  <p data-lake-id="u312f48ae" id="u312f48ae"><br></p>
  <p data-lake-id="uce6c75d4" id="uce6c75d4"><br></p>
  <p data-lake-id="ud0ef3d41" id="ud0ef3d41"><span data-lake-id="ued95c113" id="ued95c113">*/</span></p>
  <p data-lake-id="u66cc896f" id="u66cc896f"><br></p>
  <pre lang="java"><code>
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Facade {

}
</code></pre>
  <p data-lake-id="u4a5fac1e" id="u4a5fac1e"><br></p>
  <p data-lake-id="uafc78de8" id="uafc78de8"><span data-lake-id="uad58bf92" id="uad58bf92">这个注解里面没有任何参数，只用于标注那些方法要进行参数校验。</span></p>
  <p data-lake-id="u04d068c7" id="u04d068c7"><br></p>
  <p data-lake-id="u861fcb13" id="u861fcb13"><span data-lake-id="u7bd696d1" id="u7bd696d1">接下来定义切面：</span></p>
  <p data-lake-id="u667a9a06" id="u667a9a06"><br></p>
  <pre lang="java"><code>
/**
 * Facade的切面处理类，统一统计进行参数校验及异常捕获
 *
 * @author Hollis
 */
@Aspect
@Component
public class FacadeAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(FacadeAspect.class);

    @Autowired
    HttpServletRequest request;

    @Around("@annotation(com.hollis.annotation.Facade)")
    public Object facade(ProceedingJoinPoint pjp) throws Exception {

        Method method = ((MethodSignature)pjp.getSignature()).getMethod();
        Object[] args = pjp.getArgs();
           
        Class returnType = ((MethodSignature)pjp.getSignature()).getMethod().getReturnType();

        //循环遍历所有参数，进行参数校验
        for (Object parameter : args) {
            try {
                BeanValidator.validateObject(parameter);
            } catch (ValidationException e) {
                return getFailedResponse(returnType, e);
            }
        }

        try {
            // 目标方法执行
            Object response = pjp.proceed();
            return response;
        } catch (Throwable throwable) {
            return getFailedResponse(returnType, throwable);
        }
    }

    /**
     * 定义并返回一个通用的失败响应
     */
    private Object getFailedResponse(Class returnType, Throwable throwable)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //如果返回值的类型为BaseResponse 的子类，则创建一个通用的失败响应
        if (returnType.getDeclaredConstructor().newInstance() instanceof BaseResponse) {
            BaseResponse response = (BaseResponse)returnType.getDeclaredConstructor().newInstance();
            response.setSuccess(false);
            response.setResponseMessage(throwable.toString());
            response.setResponseCode(GlobalConstant.BIZ_ERROR);
            return response;
        }

        LOGGER.error(
            "failed to getFailedResponse , returnType (" + returnType + ") is not instanceof BaseResponse");
        return null;
    }
}
</code></pre>
  <p data-lake-id="u068c05d6" id="u068c05d6"><br></p>
  <p data-lake-id="u3023d01e" id="u3023d01e"><span data-lake-id="u6cede8a8" id="u6cede8a8">以上代码，和前面的切面有点类似，主要是定义了一个切面，会对所有标注@Facade的方法进行统一处理，即在开始方法调用前进行参数校验，一旦校验失败，则返回一个固定的失败的Response，特别需要注意的是，这里之所以可以返回一个固定的BaseResponse，是因为我们会要求我们的所有对外提供的接口的response必须继承BaseResponse类，这个类里面会定义一些默认的参数，如错误码等。</span></p>
  <p data-lake-id="u4983e097" id="u4983e097"><br></p>
  <p data-lake-id="ud69417e0" id="ud69417e0"><span data-lake-id="u4cf90c0b" id="u4cf90c0b">之后，只需要对需要参数校验的方法增加对应注解即可：</span></p>
  <p data-lake-id="u4e9e6bc3" id="u4e9e6bc3"><br></p>
  <pre lang="java"><code>
@Facade
public TestResponse query(User user) {
    
}
</code></pre>
  <p data-lake-id="u79e43412" id="u79e43412"><br></p>
  <p data-lake-id="u2377cc3e" id="u2377cc3e"><span data-lake-id="u3c949630" id="u3c949630">这样，有了以上注解和切面，我们就可以对所有的对外方法做统一的控制了。</span></p>
  <p data-lake-id="uaa2b060c" id="uaa2b060c"><br></p>
  <p data-lake-id="ue64cb2b6" id="ue64cb2b6"><span data-lake-id="u1086847b" id="u1086847b">其实，以上这个facadeAspect我省略了很多东西，我们真正使用的那个切面，不仅仅做了参数检查，还可以做很多其他事情。比如异常的统一处理、错误码的统一转换、记录方法执行时长、记录方法的入参出参等等。</span></p>
  <p data-lake-id="ufec48cc3" id="ufec48cc3"><br></p>
  <p data-lake-id="u0030b990" id="u0030b990"><span data-lake-id="ude9959ce" id="ude9959ce">总之，使用切面+自定义注解，我们可以统一做很多事情。除了以上的这几个场景，我们还有很多相似的用法，比如：</span></p>
  <p data-lake-id="u8266964e" id="u8266964e"><br></p>
  <p data-lake-id="ub5508620" id="ub5508620"><span data-lake-id="ua028bea8" id="ua028bea8">统一的缓存处理。如某些操作需要在操作前查缓存、操作后更新缓存。这种就可以通过自定义注解+切面的方式统一处理。</span></p>
  <p data-lake-id="u9adf64a9" id="u9adf64a9"><br></p>
  <p data-lake-id="u0c1b6142" id="u0c1b6142"><span data-lake-id="ufd0056e1" id="ufd0056e1">代码其实都差不多，思路也比较简单，就是通过自定义注解来标注需要被切面处理的累或者方法，然后在切面中对方法的执行过程进行干预，比如在执行前或者执行后做一些特殊的操作。</span></p>
  <p data-lake-id="u11fc80d5" id="u11fc80d5"><br></p>
  <p data-lake-id="ub08a2444" id="ub08a2444"><span data-lake-id="u0957a085" id="u0957a085">使用这种方式可以大大减少重复代码，大大提升代码的优雅性，方便我们使用。</span></p>
 </body>
</html>